package com.jakewharton.mosaic.animation

import androidx.annotation.FloatRange
import kotlin.math.abs
import kotlin.math.exp
import kotlin.math.ln
import kotlin.math.max

/**
 * This animation interface is intended to be stateless, just like Animation<T>. But unlike
 * Animation<T>, DecayAnimation does not have an end value defined. The end value is a result of the
 * animation rather than an input.
 */
public interface FloatDecayAnimationSpec {
	/**
	 * This is the absolute value of a velocity threshold, below which the animation is considered
	 * finished.
	 */
	public val absVelocityThreshold: Float

	/**
	 * Returns the value of the animation at the given time.
	 *
	 * @param playTimeNanos The time elapsed in milliseconds since the start of the animation
	 * @param initialValue The start value of the animation
	 * @param initialVelocity The start velocity of the animation
	 */
	public fun getValueFromNanos(
		playTimeNanos: Long,
		initialValue: Float,
		initialVelocity: Float,
	): Float

	/**
	 * Returns the duration of the decay animation, in nanoseconds.
	 *
	 * @param initialValue start value of the animation
	 * @param initialVelocity start velocity of the animation
	 */
	@Suppress("MethodNameUnits")
	public fun getDurationNanos(initialValue: Float, initialVelocity: Float): Long

	/**
	 * Returns the velocity of the animation at the given time.
	 *
	 * @param playTimeNanos The time elapsed in milliseconds since the start of the animation
	 * @param initialValue The start value of the animation
	 * @param initialVelocity The start velocity of the animation
	 */
	public fun getVelocityFromNanos(
		playTimeNanos: Long,
		initialValue: Float,
		initialVelocity: Float,
	): Float

	/**
	 * Returns the target value of the animation based on the starting condition of the animation (
	 * i.e. start value and start velocity).
	 *
	 * @param initialValue The start value of the animation
	 * @param initialVelocity The start velocity of the animation
	 */
	public fun getTargetValue(initialValue: Float, initialVelocity: Float): Float
}

private const val ExponentialDecayFriction = -4.2f

/**
 * This is a decay animation where the friction/deceleration is always proportional to the velocity.
 * As a result, the velocity goes under an exponential decay. The constructor parameter,
 * `frictionMultiplier`, can be tuned to adjust the amount of friction applied in the decay. The
 * higher the multiplier, the higher the friction, the sooner the animation will stop, and the
 * shorter distance the animation will travel with the same starting condition.
 *
 * @param frictionMultiplier The friction multiplier, indicating how quickly the animation should
 *   stop. This should be greater than `0`, with a default value of `1.0`.
 * @param absVelocityThreshold The speed at which the animation is considered close enough to rest
 *   for the animation to finish.
 */
public class FloatExponentialDecaySpec(
	@FloatRange(from = 0.0, fromInclusive = false) frictionMultiplier: Float = 1f,
	@FloatRange(from = 0.0, fromInclusive = false) absVelocityThreshold: Float = 0.1f,
) : FloatDecayAnimationSpec {

	override val absVelocityThreshold: Float = max(0.0000001f, abs(absVelocityThreshold))
	private val friction: Float = ExponentialDecayFriction * max(0.0001f, frictionMultiplier)

	override fun getValueFromNanos(
		playTimeNanos: Long,
		initialValue: Float,
		initialVelocity: Float,
	): Float {
		// TODO: Properly support nanos
		val playTimeMillis = playTimeNanos / MillisToNanos
		return initialValue - initialVelocity / friction +
			initialVelocity / friction * exp(friction * playTimeMillis / 1000f)
	}

	override fun getVelocityFromNanos(
		playTimeNanos: Long,
		initialValue: Float,
		initialVelocity: Float,
	): Float {
		// TODO: Properly support nanos
		val playTimeMillis = playTimeNanos / MillisToNanos
		return (initialVelocity * exp(((playTimeMillis / 1000f) * friction)))
	}

	@Suppress("MethodNameUnits")
	override fun getDurationNanos(initialValue: Float, initialVelocity: Float): Long {
		// Inverse of getVelocity
		return (1000f * ln(absVelocityThreshold / abs(initialVelocity)) / friction).toLong() *
			MillisToNanos
	}

	override fun getTargetValue(initialValue: Float, initialVelocity: Float): Float {
		if (abs(initialVelocity) <= absVelocityThreshold) {
			return initialValue
		}
		val duration: Double =
			ln(abs(absVelocityThreshold / initialVelocity).toDouble()) / friction * 1000

		return initialValue - initialVelocity / friction +
			initialVelocity / friction * exp((friction * duration / 1000f)).toFloat()
	}
}

/**
 * Creates a [Animation] (with a fixed start value and start velocity) that decays over time based
 * on the given [FloatDecayAnimationSpec].
 *
 * @param startValue the starting value of the fixed animation.
 * @param startVelocity the starting velocity of the fixed animation.
 */
internal fun FloatDecayAnimationSpec.createAnimation(
	startValue: Float,
	startVelocity: Float = 0f,
): Animation<Float, AnimationVector1D> {
	return DecayAnimation(this, startValue, startVelocity)
}
